Skip to content

两个定时器 setTimeout 和 setInterval

要点速览

shell
# 二者共同点
传统宏任务定时器,异步定时,脱离渲染帧。

# 二者不同点
setTimeout 延迟执行、 setInterval 间歇执行

# 二者都有明显缺点
setTimeout:受主线程阻塞、有 4ms 最小阈值、后台节流,计时不准。
setInterval:除了计时不准,后台节流,还会任务堆积、误差累积、动画卡顿、易内存泄漏,不适合做循环动画和高精度轮询。

# 替代方案
1. 普通轮询 / 倒计时 递归 setTimeout(最稳)
2. 动画 / UI 实时更新 requestAnimationFrame(最流畅)
3. 高精度计时(秒表 / 游戏) Web Worker(最准)
4. 低优先级后台任务 scheduler.postTask(最省性能)

# 总结
不可见、非活跃、被遮挡、切标签、最小化、锁屏 都是后台 定时器被节流到 1 秒左右一次 计时不准、累积误差。
setTimeout 是单纯的不准有误差
setInterval 本来就有任务堆积,再加后台节流,误差更离谱
总的来说
    setInterval 在主线程中完全不可用,只能在 Web worker 里没问题
    setTimeout 可以模拟稳定的间歇性定时,但是涉及到前后台切换的时候需要手动关闭和开启定时,需要判断是否进入后台,监听 visibilitychange 事件
    准时计时,不受前后台切换影响的情况下,可用 Web Worker 来代替

拓展

shell
# setTimeout 缺点
setInterval 最大问题是任务堆积
1. 计时不准、存在延迟 
    JS 单线程,若主线程有同步耗时任务,必须等主线程空闲才会执行,设定时间只是「最早执行时间」,不是准时执行。
2. 最小时间间隔限制
    浏览器对嵌套定时器有最小 4ms 阈值,即使设 0ms 也不是立即执行,会被强制延后。
3. 依赖事件循环队列
    受宏任务排队影响,渲染、其他脚本阻塞都会拉长实际延时。
4. 标签页后台被节流
    页面隐藏 / 切后台时,浏览器会大幅降低定时器频率,计时严重不准。

# setInterval 缺点(更严重)
setInterval 最大问题是任务堆积
1. 同样计时不准
 setTimeout 一样,受主线程阻塞、后台节流影响,间隔不稳定。
2. 任务堆积、丢帧 / 叠加执行
    不管上一次任务有没有执行完,到点就压任务,容易任务堆积;
    如果回调执行耗时 > 设定间隔,会造成任务排队堆积、连续扎堆执行,逻辑混乱、页面卡顿。
3. 无法和浏览器渲染帧对齐
    和屏幕刷新率不同步,做动画会卡顿、掉帧、抖动。
4. 误差会累积
    每次执行都有微小延迟,多次循环后时间误差越来越大。

# 解决方案
1. 递归 setTimeout(最通用、最推荐)
    原理:每次执行完再开启下一次定时器,绝对不会堆积。
    优点:稳定、无堆积、兼容所有浏览器  
    缺点:仍然受主线程阻塞影响
    适合:轮询、倒计时、普通定时
2. requestAnimationFrame(RAF)—— 做动画 / UI 首选
    原理:和浏览器刷新频率对齐(60 帧≈16ms),页面隐藏自动暂停。
    优点:超流畅、不掉帧,后台自动暂停,省电,时间更精准
    缺点:不能自定义毫秒间隔(固定 16ms 左右)
    适用:JS 动画、进度条、时钟、拖拽、UI 实时更新
3. Web Worker(真正精准定时,不受主线程阻塞)
    原理:把定时器放到独立线程运行,完全不卡主线程,最精准。
    优点:真正高精度,主线程再卡也不影响计时
    缺点:不能操作 DOM、使用稍复杂,可以通过与主线程发布事件来解决操作 dom 的问题
    适用:高精度计时器、秒表、游戏、直播计时
4. scheduler.postTask —— 现代浏览器新 API(优先级调度)
    原理:给任务设置优先级,空闲时执行,不阻塞渲染。
    优点:性能友好、不卡顿
    缺点:兼容性一般
    适用:低优先级轮询、后台统计、日志上报

setTimeout 递归 替代 setInterval

递归 setTimeout:等上一次回调执行完,再开启下一次延时,彻底解决堆积、误差累积。

js
/**
 * 递归版定时器 替代 setInterval
 * @param {Function} callback 执行回调
 * @param {number} delay 间隔毫秒
 * @returns {Object} 包含启动、停止方法
 */
function createTimer(callback, delay) {
  let timerId = null;
  let isRunning = false;

  // 递归执行
  function loop() {
    if (!isRunning) return;
    callback();
    timerId = setTimeout(loop, delay);
  }

  return {
    start() {
      if (isRunning) return;
      isRunning = true;
      loop();
    },
    stop() {
      isRunning = false;
      if (timerId) {
        clearTimeout(timerId);
        timerId = null;
      }
    }
  };
}

// 使用:创建定时器,间隔 1000ms
const timer = createTimer(() => {
  console.log('定时执行', Date.now());
}, 1000);

// 启动
timer.start();

// 需要时停止
// timer.stop();

Web Worker 实现准时秒表

js
/************** dom **************/
<div id="time">00:00.00</div>

/************** 主线程 js只需要做: 监听 Worker 事件来操作 dom **************/
// 1. 创建 Worker
const worker = new Worker('./timer.worker.js');
const timeEl = document.getElementById('time');

// 2. 接收 Worker 发来的时间,更新 DOM
worker.onmessage = (e) => {
  const { ms } = e.data;
  const formatTime = formatMs(ms);
  timeEl.innerText = formatTime; // 只在这里操作 DOM
};

// 3. 开始计时
worker.postMessage({ type: 'start' });

// 格式化时间
function formatMs(ms) {
  const minutes = String(Math.floor(ms / 60000)).padStart(2, '0');
  const seconds = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
  const cent = String(Math.floor((ms % 1000) / 10)).padStart(2, '0');
  return `${minutes}:${seconds}.${cent}`;
}

/************** Worker 线程(timer.worker.js)—— 只负责高精度计时 **************/
let startTime = 0;
let rafId = null;

// 接收主线程指令
self.onmessage = (e) => {
  if (e.data.type === 'start') {
    start();
  }
};

function start() {
  startTime = performance.now();
  loop();
}

// 高精度循环(Worker 里不受主线程阻塞)
function loop() {
  const now = performance.now();
  const ms = Math.floor(now - startTime); // 计算已过时间

  // 发送时间给主线程更新 DOM
  self.postMessage({ ms });

  // 继续循环
  rafId = requestAnimationFrame(loop);
}

切换到后台指的是什么?

js
setInterval / setTimeout 二者切后台后都会出现严重计时不准问题其实是浏览器主线程优化节流所致

// 切换到后台的场景
// 1. 浏览器内(最常见)
切换到别的标签页当前标签不可见
浏览器窗口最小化
窗口被其他窗口完全遮挡页面不可见
手机上切到别的 App / 锁屏WebView / 浏览器
// 2. 页面自身状态
document.hidden === true页面可见性 API 判断
窗口失焦blur),且页面不可见
// 3. 系统级
电脑锁屏 / 睡眠
手机锁屏 / 息屏

// 哪些情况「不算后台」(不节流)?
页面可见当前标签窗口没最小化
页面在播放音视频浏览器认为你在用不节流
 Web Worker 里的定时器Worker 不受主线程节流影响
 requestAnimationFrame后台直接暂停切回才继续不是变慢

// 怎么用代码判断「是否后台」?
console.log(document.hidden); 
// true = 后台/不可见;false = 前台/可见
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    console.log('进入后台,定时器会变慢');
  } else {
    console.log('切回前台,恢复正常');
  }
});